﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Zen4SyncReport.DAL;
using System.Data.EntityClient;
using System.Data.Objects;
using System.Linq.Expressions;

namespace Zen4SyncReport.DAL
{
    /// <summary>
    /// Provides methods to access data.
    /// This class is initialized with the Init() method and should be "disposed" when
    /// done using by calling Dispose() method.
    /// </summary>
    public class Zen4SyncDAL
    {
        #region - Constants -

        /// <summary>
        /// Template of "Provider" part of the connection string used to access Zen4SyncRepository using Entity Framework.
        /// In this string, replace {DataSource} with the Data Source and replace {LoginPolicy} with the Login Policy (Integrated Security=True) or a login and password.
        /// </summary>
        private const string ProviderConnectionStringTemplate = @"Data Source={DataSource};Initial Catalog=Zen4SyncRepository;{LoginPolicy};Pooling=False;MultipleActiveResultSets=True;";

        /// <summary>
        /// The "Metadata" part of the connection string used to access Zen4SyncRepository using Entity Framework.
        /// </summary>
        private const string ConnectionStringMetadata = @"res://*/DAL.Zen4SyncModel.csdl|res://*/DAL.Zen4SyncModel.ssdl|res://*/DAL.Zen4SyncModel.msl";

        /// <summary>
        /// The "Provider" part of the connection string used to access Zen4SyncRepository using Entity Framework.
        /// </summary>
        private const string ConnectionStringProvider = "System.Data.SqlClient";

        #endregion - Constants -

        #region - Private fields -

        /// <summary>
        /// Private field associated to <see cref="Repository"/>.
        /// </summary>
        private Zen4SyncRepositoryEntities _Repository;

        #endregion - Private fields -

        #region - Properties -

        /// <summary>
        /// Unique way to access the data.
        /// </summary>
        private Zen4SyncRepositoryEntities Repository
        {
            get
            {
                if (!this.IsInitialized)
                    throw new Exception("Instance of Zen4SyncDAL must be initialized before calling methods. Call Init() method.");

                return _Repository;
            }
        }

        /// <summary>
        /// Indicates if the instance is properly initialized.
        /// To initialize the instance, see the Init() method.
        /// </summary>
        public bool IsInitialized { get; private set; }

        /// <summary>
        /// Gets the Exception that occured when performing the initialization of the instance.
        /// null if no exception occured.
        /// </summary>
        public Exception InitException { get; private set; }
        
        #endregion - Properties -

        #region - Constructors and initialization -

        /// <summary>
        /// Default constructor.
        /// Note: call Init() method before using the instance.
        /// Note2: Call Dispose() method when done using the instance.
        /// </summary>
        public Zen4SyncDAL()
        {
            this.IsInitialized = false;
        }

        /// <summary>
        /// Initializes and tests the connection to the database.
        /// If the connection fails, false is returned and the exception which occured can be retrieved using the <see cref="InitException"/> property.
        /// </summary>
        /// <param name="serverName">Name of the server hosting Zen4SyncRepository.</param>
        /// <param name="useIntegratedSecurity">true to use Integrated Security, false to use the given login and password.</param>
        /// <param name="login">Login when not using Integrated Security. Give null or empty String when using Integrated Security.</param>
        /// <param name="password">Password when not using Integrated Security. Give null or empty String when using Integrated Security.</param>
        /// <returns>
        /// true is the connection is successful, false otherwise.
        /// If false is returned, the Exception that occured is accessible throug the InitException property.
        /// </returns>
        public bool Init(string serverName, bool useIntegratedSecurity, string login, string password)
        {
            // Set up connection string
            String providerConnectionString = ProviderConnectionStringTemplate;
            providerConnectionString = providerConnectionString.Replace("{DataSource}", serverName);
            if (useIntegratedSecurity)
                providerConnectionString = providerConnectionString.Replace("{LoginPolicy}", "Integrated Security=True");
            else
                providerConnectionString = providerConnectionString.Replace("{LoginPolicy}", String.Format("User Id={0};Password={1}", login, password));

            EntityConnectionStringBuilder sb = new EntityConnectionStringBuilder();
            sb.Metadata = ConnectionStringMetadata;
            sb.Provider = ConnectionStringProvider;
            sb.ProviderConnectionString = providerConnectionString;

            // Testing the connection
            try
            {
                this._Repository = new Zen4SyncRepositoryEntities(sb.ToString());
                this._Repository.Connection.Open();
                this.IsInitialized = true;
                return true;
            }
            catch (Exception exc)
            {
                this.InitException = exc;
                this._Repository = null;
                return false;
            }
            finally
            {
                // Close the connection in case of a successful test
                if (this._Repository != null && this._Repository.Connection.State == System.Data.ConnectionState.Open)
                    this._Repository.Connection.Close();
            }
        }

        #endregion - Constructors and initialization -

        #region - Methods -

        /// <summary>
        /// Disposes all the resources used by the instance.
        /// </summary>
        public void Dispose()
        {
            if (this.IsInitialized)
            {
                this._Repository.Dispose();
                this._Repository = null;
                this.IsInitialized = false;
            }
        }

        /// <summary>
        /// Returns all the Test Session Categories, ordered by ascending name.
        /// </summary>
        /// <returns>All the Test Session Categories.</returns>
        public List<testSessionCategory> GetAllTestSessionCategories()
        {
            return
                Repository.testSessionCategories
                .OrderBy(cat => cat.testSessionCategory_name)
                .ToList();
        }

        /// <summary>
        /// Searches among all the Test Sessions and return the ones matching the given restrictions.
        /// Matching Test Sessions are ordered by ascending name.
        /// If a given restriction is null, it won't be applied.
        /// </summary>
        /// <param name="restrictions">A restriction that Test Sessions must match.</param>
        /// <returns>List of Test Sessions matching the given restrictions.</returns>
        public List<testSession> SearchTestSessions(params Func<testSession, bool>[] restrictions)
        {
            IEnumerable<testSession> matches = Repository.testSession;

            foreach (var restriction in restrictions.Where(r => r!= null))
                matches = matches.Where(restriction);

            return
                matches
                .OrderBy(s => s.testSession_name)
                .ToList();
        }

        /// <summary>
        /// Gets the Test Session whose ID matches the given Guid.
        /// </summary>
        /// <param name="testSessionId">ID of the target Test Session.</param>
        /// <returns>The Test Session matching the given ID, or null if no match is found.</returns>
        public testSession GetTestSession(Guid testSessionId)
        {
            return Repository.testSession
                .Where(t => t.testSession_id == testSessionId)
                .SingleOrDefault();
        }

        /// <summary>
        /// Gets the statistics for the given activity.
        /// Statistics in the returned array are :
        /// [0]: Average Execution Time
        /// [1]: Slowest Client Time
        /// [2]: Fastest Client Time
        /// [3]: Delta between Slowest Client and Fastest Client
        /// </summary>
        /// <param name="activityId">The ID of the Activity whose stats will be computed.</param>
        /// <param name="testScenarioId">The ID of the Test Scenario in which the Activity was executed.</param>
        /// <param name="testSessionId">The ID of the Test Session in which the Activity was executed.</param>
        /// <returns></returns>
        public int[] GetActivityStats(Guid activityId, Guid testScenarioId, Guid testSessionId)
        {
            int[] stats = new int[4];

            // Average execution time
            ObjectParameter avgExecutionTime = new ObjectParameter("out_averageExecutionTime", typeof(Int32));
            this.Repository.AverageExecutionTime(testSessionId, testScenarioId, activityId, avgExecutionTime);
            stats[0] = Int32.Parse(avgExecutionTime.Value.ToString());

            // Slowest client time
            ObjectParameter slowestExecutionTime = new ObjectParameter("out_slowestExecutionTime", typeof(Int32));
            this.Repository.SlowestActivityExecutionTime(testSessionId, testScenarioId, activityId, slowestExecutionTime);
            stats[1] = Int32.Parse(slowestExecutionTime.Value.ToString());

            // Fastest client time
            ObjectParameter fastestExecutionTime = new ObjectParameter("out_fastestExecutionTime", typeof(Int32));
            this.Repository.FastestActivityExecutionTime(testSessionId, testScenarioId, activityId, fastestExecutionTime);
            stats[2] = Int32.Parse(fastestExecutionTime.Value.ToString());

            // Delta
            stats[3] = stats[1] - stats[2];

            return stats;
        }

        /// <summary>
        /// Gets the count of Client Sessions involved in the given Test Scenario executed in the given Test Session.
        /// </summary>
        /// <param name="testSessionId">ID of the Test Session executing the Test Scenario.</param>
        /// <param name="testScenarioId">ID of the Test Scenario executed.</param>
        /// <returns>Number of Client Sessions involved in the given Test Scenario executed in the given Test Session.</returns>
        public int GetClientSessionsInvolvedCount(Guid testSessionId, Guid testScenarioId)
        {
            return Repository.clientSession
                .Where(c =>
                    c.clientSession_testScenarioId == testScenarioId
                    &&
                    c.clientSession_testSessionId == testSessionId)
                .Count();
        }

        /// <summary>
        /// Gets all the errors for the given Test Scenario executed in the given Test Session.
        /// Errors are sorted on the Client Name ascending.
        /// </summary>
        /// <param name="testSession">ID of the Test Session owning the Test Scenario.</param>
        /// <param name="testScenario">ID of the Test Scenario.</param>
        /// <returns>
        /// All the errors for the given Test Scenario executed in the given Test Session.
        /// Errors are wraped in the <see cref="ClientError"/> class.
        /// </returns>
        public List<ClientError> GetClientsErrors(testSession testSession, testScenario testScenario)
        {
            List<ClientError> errors = new List<ClientError>();

            // Get all the Activity History whose error column is not null
            List<activityHistory> historiesOnError =
                testSession.clientSession
                .SelectMany(client => client.activityHistory)
                .Where(history => !String.IsNullOrEmpty(history.activityHistory_error))
                .OrderBy(history => history.clientSession.clientSession_name)
                .ToList();

            foreach (activityHistory historyOnError in historiesOnError)
            {
                ClientError err = new ClientError
                {
                    ActivityId = historyOnError.activityHistory_activityId,
                    ActivityType = historyOnError.activity.HumanReadableType,
                    ClientId = historyOnError.activityHistory_clientSessionId,
                    ClientName = historyOnError.clientSession.clientSession_name,
                    Error = historyOnError.activityHistory_error
                };
                errors.Add(err);
            }

            return errors;
        }

        #endregion - Methods -
    }
}
